# -*- coding: utf-8 -*-
#
import uuid

from django.db.models.signals import (
    post_save, m2m_changed, pre_delete, pre_save
)
from django.dispatch import receiver
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from django.utils.functional import LazyObject
from django.contrib.auth import BACKEND_SESSION_KEY
from django.utils.translation import ugettext_lazy as _
from django.utils import translation
from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request

from users.models import User
from assets.models import Asset, SystemUser, CommandFilter
from terminal.models import Session, Command
from perms.models import AssetPermission, ApplicationPermission
from rbac.models import Role

from audits.utils import model_to_dict_for_operate_log as model_to_dict
from audits.handler import (
    get_instance_current_with_cache_diff, cache_instance_before_data,
    create_or_update_operate_log, get_instance_dict_from_cache
)
from authentication.signals import post_auth_failed, post_auth_success
from authentication.utils import check_different_city_login_if_need
from jumpserver.utils import current_request
from users.signals import post_user_change_password
from .utils import write_login_log
from . import models, serializers
from .models import OperateLog
from .const import MODELS_NEED_RECORD
from terminal.backends.command.serializers import SessionCommandSerializer
from terminal.serializers import SessionSerializer
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.utils import get_request_ip, get_logger, get_syslogger
from common.utils.encode import data_to_json


logger = get_logger(__name__)
sys_logger = get_syslogger(__name__)
json_render = JSONRenderer()


class AuthBackendLabelMapping(LazyObject):
    @staticmethod
    def get_login_backends():
        backend_label_mapping = {}
        for source, backends in User.SOURCE_BACKEND_MAPPING.items():
            for backend in backends:
                backend_label_mapping[backend] = source.label
        backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
        backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
        backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
        backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
        backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
        backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu')
        backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
        backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
        return backend_label_mapping

    def _setup(self):
        self._wrapped = self.get_login_backends()


AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()

M2M_ACTION = {
    POST_ADD: OperateLog.ACTION_CREATE,
    POST_REMOVE: OperateLog.ACTION_DELETE,
    POST_CLEAR: OperateLog.ACTION_DELETE,
}


@receiver(m2m_changed)
def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
    if action not in M2M_ACTION:
        return
    if not instance:
        return

    resource_type = instance._meta.verbose_name
    current_instance = model_to_dict(instance, include_model_fields=False)

    instance_id = current_instance.get('id')
    log_id, before_instance = get_instance_dict_from_cache(instance_id)

    field_name = str(model._meta.verbose_name)
    objs = model.objects.filter(pk__in=pk_set)
    objs_display = [str(o) for o in objs]
    action = M2M_ACTION[action]
    changed_field = current_instance.get(field_name, [])

    after, before, before_value = None, None, None
    if action == OperateLog.ACTION_CREATE:
        before_value = list(set(changed_field) - set(objs_display))
    elif action == OperateLog.ACTION_DELETE:
        before_value = list(
            set(changed_field).symmetric_difference(set(objs_display))
        )

    if changed_field:
        after = {field_name: changed_field}
    if before_value:
        before = {field_name: before_value}

    if sorted(str(before)) == sorted(str(after)):
        return

    create_or_update_operate_log(
        OperateLog.ACTION_UPDATE, resource_type,
        resource=instance, log_id=log_id, before=before, after=after
    )


def signal_of_operate_log_whether_continue(sender, instance, created, update_fields=None):
    condition = True
    if not instance:
        condition = False
    if instance and getattr(instance, SKIP_SIGNAL, False):
        condition = False
    # 终端模型的 create 事件由系统产生，不记录
    if instance._meta.object_name == 'Terminal' and created:
        condition = False
    # last_login 改变是最后登录日期, 每次登录都会改变
    if instance._meta.object_name == 'User' and \
            update_fields and 'last_login' in update_fields:
        condition = False
    # 不在记录白名单中，跳过
    if sender._meta.object_name not in MODELS_NEED_RECORD:
        condition = False
    return condition


@receiver(pre_save)
def on_object_pre_create_or_update(sender, instance=None, raw=False, using=None, update_fields=None, **kwargs):
    ok = signal_of_operate_log_whether_continue(
        sender, instance, False, update_fields
    )
    if not ok:
        return
    instance_before_data = {'id': instance.id}
    raw_instance = type(instance).objects.filter(pk=instance.id).first()
    if raw_instance:
        instance_before_data = model_to_dict(raw_instance)
    operate_log_id = str(uuid.uuid4())
    instance_before_data['operate_log_id'] = operate_log_id
    setattr(instance, 'operate_log_id', operate_log_id)
    cache_instance_before_data(instance_before_data)


@receiver(post_save)
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
    ok = signal_of_operate_log_whether_continue(
        sender, instance, created, update_fields
    )
    if not ok:
        return

    log_id, before, after = None, None, None
    if created:
        action = models.OperateLog.ACTION_CREATE
        after = model_to_dict(instance)
        log_id = getattr(instance, 'operate_log_id', None)
    else:
        action = models.OperateLog.ACTION_UPDATE
        current_instance = model_to_dict(instance)
        log_id, before, after = get_instance_current_with_cache_diff(current_instance)

    resource_type = sender._meta.verbose_name
    create_or_update_operate_log(
        action, resource_type, resource=instance,
        log_id=log_id, before=before, after=after
    )


@receiver(pre_delete)
def on_object_delete(sender, instance=None, **kwargs):
    ok = signal_of_operate_log_whether_continue(sender, instance, False)
    if not ok:
        return

    resource_type = sender._meta.verbose_name
    create_or_update_operate_log(
        models.OperateLog.ACTION_DELETE, resource_type,
        resource=instance, before=model_to_dict(instance)
    )


@receiver(post_user_change_password, sender=User)
def on_user_change_password(sender, user=None, **kwargs):
    if not current_request:
        remote_addr = '127.0.0.1'
        change_by = 'System'
    else:
        remote_addr = get_request_ip(current_request)
        if not current_request.user.is_authenticated:
            change_by = str(user)
        else:
            change_by = str(current_request.user)
    with transaction.atomic():
        models.PasswordChangeLog.objects.create(
            user=str(user), change_by=change_by,
            remote_addr=remote_addr,
        )


def on_audits_log_create(sender, instance=None, **kwargs):
    if sender == models.UserLoginLog:
        category = "login_log"
        serializer_cls = serializers.UserLoginLogSerializer
    elif sender == models.FTPLog:
        category = "ftp_log"
        serializer_cls = serializers.FTPLogSerializer
    elif sender == models.OperateLog:
        category = "operation_log"
        serializer_cls = serializers.OperateLogSerializer
    elif sender == models.PasswordChangeLog:
        category = "password_change_log"
        serializer_cls = serializers.PasswordChangeLogSerializer
    elif sender == Session:
        category = "host_session_log"
        serializer_cls = SessionSerializer
    elif sender == Command:
        category = "session_command_log"
        serializer_cls = SessionCommandSerializer
    else:
        return

    serializer = serializer_cls(instance)
    data = data_to_json(serializer.data, indent=None)
    msg = "{} - {}".format(category, data)
    sys_logger.info(msg)


def get_login_backend(request):
    backend = request.session.get('auth_backend', '') or \
              request.session.get(BACKEND_SESSION_KEY, '')

    backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None)
    if backend_label is None:
        backend_label = ''
    return backend_label


def generate_data(username, request, login_type=None):
    user_agent = request.META.get('HTTP_USER_AGENT', '')
    login_ip = get_request_ip(request) or '0.0.0.0'

    if login_type is None and isinstance(request, Request):
        login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
    if login_type is None:
        login_type = 'W'

    with translation.override('en'):
        backend = str(get_login_backend(request))

    data = {
        'username': username,
        'ip': login_ip,
        'type': login_type,
        'user_agent': user_agent[0:254],
        'datetime': timezone.now(),
        'backend': backend,
    }
    return data


@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
    logger.debug('User login success: {}'.format(user.username))
    check_different_city_login_if_need(user, request)
    data = generate_data(user.username, request, login_type=login_type)
    request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
    data.update({'mfa': int(user.mfa_enabled), 'status': True})
    write_login_log(**data)


@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason='', **kwargs):
    logger.debug('User login failed: {}'.format(username))
    data = generate_data(username, request)
    data.update({'reason': reason[:128], 'status': False})
    write_login_log(**data)
